package com.sfx.shortLink.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.sfx.shortLink.common.constants.RedisKeyConstant;
import com.sfx.shortLink.common.convention.exception.ClientException;
import com.sfx.shortLink.common.convention.exception.ServiceException;
import com.sfx.shortLink.common.database.BaseDO;
import com.sfx.shortLink.common.enums.ValidDateTypeEnums;
import com.sfx.shortLink.common.utils.HashUtil;
import com.sfx.shortLink.common.utils.LinkUtils;
import com.sfx.shortLink.dao.entity.*;
import com.sfx.shortLink.dao.mapper.*;
import com.sfx.shortLink.dto.ShortLinkStatsRecordDTO;
import com.sfx.shortLink.dto.req.ShortLinkCreateReqDO;
import com.sfx.shortLink.dto.req.ShortLinkPageReqDTO;
import com.sfx.shortLink.dto.req.ShortLinkUpdateReqDTO;
import com.sfx.shortLink.dto.resp.ShortLinkCreateRespDTO;
import com.sfx.shortLink.dto.resp.ShortLinkGroupCountRespDTO;
import com.sfx.shortLink.dto.resp.ShortLinkPageRespDTO;
import com.sfx.shortLink.mq.producer.ShortLinkStatsSaveProducer;
import com.sfx.shortLink.rmq.topic.TopicConstants;
import com.sfx.shortLink.service.IShortLinkService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.MQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;


@Service
@Slf4j
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ILinkMapper, ShortLinkDO>
        implements IShortLinkService {
    private final Logger LOGGER = LoggerFactory.getLogger(ShortLinkServiceImpl.class);

    private final RBloomFilter<String> shortLinkUriBloomFilter;
    private final StringRedisTemplate stringRedisTemplate;
    private final RedissonClient redissonClient;
    private final ILinkGotoMapper linkGotoMapper;
    private final ILinkAccessLogsMapper linkAccessLogsMapper;
    private final ILinkLocaleStatsMapper linkLocaleStatsMapper;
    private final ILinkAccessStatsMapper linkAccessStatsMapper;
    private final ILinkOsStatsMapper linkOsStatsMapper;
    private final ILinkBrowserMapper linkBrowserMapper;
    private final ILinkDeviceStatsMapper linkDeviceStatsMapper;
    private final ILinkNetworkStatsMapper linkNetworkStatsMapper;
    private final ILinkStatsTodayMapper linkStatsTodayMapper;
    private final ShortLinkStatsSaveProducer linkStatsSaveProducer;

    @Value("${short-link.stats.locale.amap-key}")
    private String amapKey;

    @Value("${short-link.domain.default}")
    private String SHORT_LINK_DEFAULT_DOMAIN;

    private final MQProducer mqProducer;


    /**
     * @param requestParam 生成短链接请求参数
     */
    @Override
    public ShortLinkCreateRespDTO createShortLink1(ShortLinkCreateReqDO requestParam) {
        // 1. 生成短链接前缀
        String shortUrI = getSuffix1(SHORT_LINK_DEFAULT_DOMAIN, requestParam.getOriginUrl());
        // 2. 得到fullShortUrl
        // nurl.link/ll
        String fullShortUrl = SHORT_LINK_DEFAULT_DOMAIN + "/" + shortUrI;
        // 3. 创建shortLinkDO对象,然后插入到数据库中
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .fullShortUrl(fullShortUrl)
                .shortUri(shortUrI)
                .gid(requestParam.getGid())
                .originUrl(requestParam.getOriginUrl())
                .domain(SHORT_LINK_DEFAULT_DOMAIN)
                .describe(requestParam.getDescribe())
                .createdType(0)
                .enableStatus(0)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .totalPv(0)
                .totalUip(0)
                .totalUv(0)
                .validDateType(0)
                .validDate(null)
                .clickNum(0)
                .build();

        ShortLinkGotoDO shortLinkGotoDO = ShortLinkGotoDO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl(fullShortUrl)
                .build();

        try {
            baseMapper.insert(shortLinkDO);
            linkGotoMapper.insert(shortLinkGotoDO);
        } catch (DuplicateKeyException exception) {
            log.warn("短链接生成重复!!!");
            throw new ServiceException("生成短链接重复");
        }
        // 5. 加入到布隆过滤器
        shortLinkUriBloomFilter.add(fullShortUrl);
        // 4. 缓存预热
        stringRedisTemplate.opsForValue().set(
                String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                shortLinkDO.getOriginUrl(),
                LinkUtils.getValidExpireTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
        );
        return ShortLinkCreateRespDTO.builder()
                .gid(requestParam.getGid())
                .fullShortUrl("http://" + fullShortUrl)
                .originUrl(requestParam.getOriginUrl())
                .build();
    }

    /**
     * 通过hash算法得到一个后缀 + 并且是的后缀不能重复
     *
     * @param domain    域名
     * @param originUrl 原始url
     * @return 根据哈希算法得到shortLinkURI
     */
    private String getSuffix1(String domain, String originUrl) {
        String shortUri = "";
        int retryGenerateShortUriCount = 0;
        for (; ; ) {
            if (retryGenerateShortUriCount >= 10) {
                throw new ServiceException("生成shortLinkURI频繁,请稍后重试 !!!");
            }
            originUrl = originUrl + UUID.randomUUID().toString();
            shortUri = HashUtil.hashToBase62(originUrl);
            // 如果在过滤器中不存在,直接返回 -->短链接唯一性
            if (!shortLinkUriBloomFilter.contains(domain + "/" + shortUri)) {
                break;
            }
            retryGenerateShortUriCount++;
        }
        return shortUri;
    }


    @Override
    public IPage<ShortLinkPageRespDTO> shortLinkPage(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }


    @Override
    public List<ShortLinkGroupCountRespDTO> getGroupShortLinkCount(List<String> requestParam) {
        if (requestParam == null || CollectionUtils.isEmpty(requestParam)) {
            throw new ClientException("参数有误");
        }
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(ShortLinkDO.builder().build())
                .select("gid as gid , count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .in("gid", requestParam)
                .groupBy("gid");
        List<Map<String, Object>> mapList = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(mapList, ShortLinkGroupCountRespDTO.class);
    }


    /**
     * 要修改,也就是只能修改短链接分组 + 描述 + 有效期
     *
     * @param requestParam 请求参数
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(RedisKeyConstant.LOCK_GID_UPDATE_KEY + requestParam.getFullShortUrl());
        RLock rLock = readWriteLock.writeLock();
        if (!rLock.tryLock()) {
            throw new ServiceException("短链接正在修改,请稍后统计");
        }
        try {
            // 1. 查询短链接是否存在
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    .eq(ShortLinkDO::getDelTime, 0);
            ShortLinkDO hasShortLink = baseMapper.selectOne(queryWrapper);
            if (hasShortLink == null) {
                throw new ClientException("该短链接不存在,无法进行修改");
            }

            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .favicon(hasShortLink.getFavicon())
                    .shortUri(hasShortLink.getShortUri())
                    .fullShortUrl(hasShortLink.getFullShortUrl())
                    .domain(hasShortLink.getDomain())
                    .clickNum(hasShortLink.getClickNum())
                    .originUrl(hasShortLink.getOriginUrl())
                    .totalPv(hasShortLink.getTotalPv())
                    .totalUip(hasShortLink.getTotalUip())
                    .totalUv(hasShortLink.getTotalUv())
                    .enableStatus(hasShortLink.getEnableStatus())

                    // 修改短链接描述
                    .describe(requestParam.getDescribe())
                    // 修改短链接分组
                    .gid(requestParam.getGid())
                    // 修改短链接有效期
                    .validDate(requestParam.getValidDate())
                    .validDateType(requestParam.getValidDateType())
                    .build();

            // 2. 当前短链接和要修改的短链接 分组是否是一致的
            if (Objects.equals(hasShortLink.getGid(), requestParam.getGid())) {
                // 3. 如果是一致的,直接更新信息
                LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getGid, requestParam.getGid())
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getEnableStatus, 0)
                        .eq(ShortLinkDO::getDelTime, 0)
                        // 如果为空设置为null
                        .set(Objects.equals(requestParam.getValidDateType(), ValidDateTypeEnums.PERMANENT), ShortLinkDO::getValidDate, null);
                baseMapper.update(shortLinkDO, updateWrapper);
            } else {
                try {
                    // 4. 如果不是一致的,那么直接删除原来的分组,新增新的分组
                    LambdaUpdateWrapper<ShortLinkDO> shortLinkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                            .eq(ShortLinkDO::getGid, hasShortLink.getGid())
                            .eq(ShortLinkDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(ShortLinkDO::getEnableStatus, 0)
                            .eq(ShortLinkDO::getDelTime, 0)
                            .set(BaseDO::getDelFlag, 1)
                            .set(ShortLinkDO::getDelTime, new Date());
                    // 删除原来的分组
                    baseMapper.update(null, shortLinkUpdateWrapper);
                    shortLinkDO.setDelFlag(0);
                    // 新增新来的分组
                    baseMapper.insert(shortLinkDO);

                    LambdaUpdateWrapper<LinkStatsTodayDO> todayUpdateWrapper = Wrappers.lambdaUpdate(LinkStatsTodayDO.class)
                            .eq(LinkStatsTodayDO::getGid, hasShortLink.getGid())
                            .eq(LinkStatsTodayDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .set(BaseDO::getDelFlag, 1);
                    LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                            .build();
                    linkStatsTodayMapper.update(null, todayUpdateWrapper);
                    linkStatsTodayDO.setGid(requestParam.getGid());
                    linkStatsTodayDO.setDelFlag(0);
                    linkStatsTodayMapper.insert(linkStatsTodayDO);

                    LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                            .eq(ShortLinkGotoDO::getGid, hasShortLink.getGid())
                            .eq(ShortLinkGotoDO::getFullShortUrl, hasShortLink.getFullShortUrl());
                    ShortLinkGotoDO shortLinkGotoDO = linkGotoMapper.selectOne(linkGotoQueryWrapper);
                    linkGotoMapper.deleteById(shortLinkGotoDO);
                    shortLinkGotoDO.setGid(requestParam.getGid());
                    linkGotoMapper.insert(shortLinkGotoDO);

                    LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                            .eq(LinkAccessStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkAccessStatsDO::getGid, hasShortLink.getGid());

                    LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);

                    LambdaUpdateWrapper<LinkLocaleStatsDO> localeStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                            .eq(LinkLocaleStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkLocaleStatsDO::getGid, hasShortLink.getGid());
                    LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkLocaleStatsMapper.update(linkLocaleStatsDO, localeStatsUpdateWrapper);

                    LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                            .eq(LinkOsStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkOsStatsDO::getGid, hasShortLink.getGid());
                    LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);

                    LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                            .eq(LinkBrowserStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkBrowserStatsDO::getGid, hasShortLink.getGid());
                    LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkBrowserMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);

                    LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                            .eq(LinkDeviceStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkDeviceStatsDO::getGid, hasShortLink.getGid());
                    LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceUpdateWrapper);

                    LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                            .eq(LinkNetworkStatsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkNetworkStatsDO::getGid, hasShortLink.getGid());
                    LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);

                    LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                            .eq(LinkAccessLogsDO::getFullShortUrl, hasShortLink.getFullShortUrl())
                            .eq(LinkAccessLogsDO::getGid, hasShortLink.getGid());
                    LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                            .gid(requestParam.getGid())
                            .build();
                    linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
                } catch (Throwable e) {
                    log.error("短链接修改出现异常", e);
                }
            }
        } finally {
            rLock.unlock();
        }
    }


    /**
     * 短链接跳转到原始链接
     *
     * @param shortUri 短链接后缀
     * @param request  请求
     * @param response 响应
     */
    @Override
    public void restoreUrl1(String shortUri, HttpServletRequest request, HttpServletResponse response) {
        String fullShortUrl = request.getServerName() + ":" + request.getServerPort() + "/" + shortUri;
        try {
            // 1. 查询缓存是否存在,缓存存在直接返回
            String originUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(originUrl)) {
                ShortLinkStatsRecordDTO statsRecordDTO = buildShortLinkStatsRecordDTO(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, null, statsRecordDTO);
                response.sendRedirect(originUrl);
                return;
            }
            // 2. 布隆过滤器是否不存在,如果不存在直接404
            if (!shortLinkUriBloomFilter.contains(fullShortUrl)) {
                response.sendRedirect("/page/notfound");
                return;
            }
            // 3. 是否缓存了空对象
            String emptyUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
            if (StrUtil.isNotBlank(emptyUrl)) {
                response.sendRedirect("/page/notfound");
                return;
            }
            // 4. 如果以上都没有那么我们加入分布式锁,确保只有一个线程访问到数据库
            RLock lock = redissonClient.getLock(String.format(RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
            lock.lock();
            try {
                // 5. 二次校验
                originUrl = stringRedisTemplate.opsForValue().get(String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl));
                if (StrUtil.isNotBlank(originUrl)) {
                    ShortLinkStatsRecordDTO statsRecordDTO = buildShortLinkStatsRecordDTO(fullShortUrl, request, response);
                    shortLinkStats(fullShortUrl, null, statsRecordDTO);
                    response.sendRedirect(originUrl);
                    return;
                }
                // 6. 因为要跳转所以需要一个路由表找到gid
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                ShortLinkGotoDO shortLinkGotoDO = linkGotoMapper.selectOne(linkGotoQueryWrapper);
                if (shortLinkGotoDO == null) {
                    // 7. 不存在,直接返回404 , 缓存空对象
                    stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 3, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                String gid = shortLinkGotoDO.getGid();
                // 8. 查询数据库短链接,不存在缓存空对象
                LambdaQueryWrapper<ShortLinkDO> shortLinkQueryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                        .eq(ShortLinkDO::getGid, gid)
                        .eq(BaseDO::getDelFlag, 0);
                ShortLinkDO shortLinkDO = baseMapper.selectOne(shortLinkQueryWrapper);
                if (shortLinkDO == null) {
                    stringRedisTemplate.opsForValue().set(String.format(RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 3, TimeUnit.MINUTES);
                    response.sendRedirect("/page/notfound");
                    return;
                }
                // 9. 存在,加入缓存,302重定向
                stringRedisTemplate.opsForValue().set(
                        String.format(RedisKeyConstant.GOTO_SHORT_LINK_KEY, fullShortUrl),
                        shortLinkDO.getOriginUrl(),
                        LinkUtils.getValidExpireTime(shortLinkDO.getValidDate()), TimeUnit.MILLISECONDS
                );
                ShortLinkStatsRecordDTO statsRecordDTO = buildShortLinkStatsRecordDTO(fullShortUrl, request, response);
                shortLinkStats(fullShortUrl, gid, statsRecordDTO);
                response.sendRedirect(originUrl);
            } finally {
                // 10. 释放分布式锁
                lock.unlock();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /**
     * 提前准备一些数据用于进行统计
     */
    private ShortLinkStatsRecordDTO buildShortLinkStatsRecordDTO(String fullShortUrl, HttpServletRequest request, HttpServletResponse response) {
        // uvFlag
        AtomicBoolean uvFlag = new AtomicBoolean();
        // 浏览器怎么知道我是同一个用户访问呢 ?  这里通过cookie技术做
        AtomicReference<String> user = new AtomicReference<>();
        // 先拿到所有的cookie,看cookie中有没有uvCookie
        Cookie[] cookies = request.getCookies();
        Runnable cookieUv = () -> {
            String userInfoId = UUID.randomUUID().toString();
            Cookie cookie = new Cookie("uvCookie", userInfoId);
            cookie.setPath("/" + fullShortUrl.substring(fullShortUrl.indexOf("/") + 1));
            cookie.setMaxAge(30 * 24 * 60 * 60);
            response.addCookie(cookie);
            user.set(userInfoId);
            stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl, userInfoId);
            stringRedisTemplate.expire(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl,12, TimeUnit.HOURS);
            uvFlag.set(Boolean.TRUE);
        };
        if (ArrayUtil.isEmpty(cookies)) {
            cookieUv.run();
        } else {
            Arrays.stream(cookies)
                    .filter(ck -> Objects.equals(ck.getName(), "uvCookie"))
                    .findFirst().map(Cookie::getValue)
                    .ifPresentOrElse(val -> {
                        Long uv = stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl, val);
                        stringRedisTemplate.expire(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl,12, TimeUnit.HOURS);
                        // 如果能够设置成功,那么就证明第一次访问
                        uvFlag.set(uv != null && uv > 0L);
                        user.set(val);
                    }, cookieUv);
        }
        // ip
        String ip = LinkUtils.getActualIp((HttpServletRequest) request);
        // uip
        Long uipAdded = stringRedisTemplate.opsForSet().add(RedisKeyConstant.SHORT_LINK_STATS_UIP_KEY + fullShortUrl, ip);
        stringRedisTemplate.expire(RedisKeyConstant.SHORT_LINK_STATS_UV_KEY + fullShortUrl,12, TimeUnit.HOURS);
        // netWork
        String network = LinkUtils.getNetwork(request);
        // os
        String os = LinkUtils.getOs(request);
        // browser
        String browser = LinkUtils.getBrowser(request);
        // device
        String device = LinkUtils.getDevice(request);
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .ip(ip)
                .os(os)
                .network(network)
                .user(user.get())
                .device(device)
                .browser(browser)
                .uipFlag(uipAdded != null && uipAdded > 0)
                .uvFlag(uvFlag.get())
                .build();
    }


    public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecordDTO) {
        Map<String, Object> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl", fullShortUrl);
        producerMap.put("gid", gid);
        producerMap.put("statsRecordDTO", statsRecordDTO);
        Message message = new Message();
        message.setBody(JSON.toJSONBytes(producerMap));
        message.setTopic(TopicConstants.SHORT_LINK_STATS_TOPIC_KEY);
        try {
            SendResult sendResult = mqProducer.send(message);
            LOGGER.info("[ShortLinkServiceImpl] message is {}", sendResult);
        } catch (Exception e) {
            LOGGER.error("[ShortLinkServiceImpl] send message error", e);
        }
    }


    @SneakyThrows
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }
}